Keys vs Scan
contents
keys와 scan은 프로덕션 Redis 인스턴스를 관리할 때 이해해야 할 가장 중요한 차이점 중 하나입니다.
짧은 답변: KEYS는 위험한 블로킹(Blocking) 작업으로, 프로덕션 서버를 멈추게(Freeze) 할 수 있습니다. SCAN은 프로덕션 환경을 위해 설계된 안전한 논블로킹(Non-blocking) 대안입니다.
다음은 이들이 내부적으로 어떻게 작동하는지에 대한 상세한 비교입니다.
1. KEYS 명령어 (핵폭탄급 옵션) ☢️
KEYS pattern 명령어는 특정 패턴과 일치하는 모든 키를 한 번에 반환합니다.
작동 방식
Redis는 싱글 스레드(Single-threaded) 입니다. 즉, Redis는 한 번에 하나의 명령어만 처리합니다. 해당 명령어가 끝날 때까지 다른 작업은 아무것도 할 수 없습니다.
KEYS *를 실행하면, Redis는 전체 키 공간(메인 해시 테이블)에 대한 루프를 시작합니다.- 모든 단일 키를 하나씩 확인하여 패턴과 일치하는지 검사합니다.
- 일치하는 모든 키를 담은 거대한 리스트를 만들어 한 번에 응답으로 보냅니다.
위험한 이유
- 시간 복잡도: $O(N)$ ($N$은 전체 키의 개수).
- 블로킹 동작: 만약 1,000만 개의 키가 있다면,
KEYS는 실행되는 데 2초가 걸릴 수 있습니다. 그 2초 동안 애플리케이션은 다운된 상태가 됩니다. 다른 클라이언트는 읽기나 쓰기를 할 수 없으며, 웹 요청은 타임아웃됩니다. - 메모리 급증: Redis는 사용자에게 반환하기 위해 모든 키를 담은 버퍼를 생성해야 합니다. 결과 집합이 거대하다면, 이로 인해 메모리가 고갈될 수 있습니다.
사용해야 할 때
- 개발 / 디버깅: 로컬 노트북에서 키를 확인할 때.
- 빈 데이터베이스: $N$이 매우 작다는 것을 확실히 알 때.
- 프로덕션에서는 절대 사용 금지.
2. SCAN 명령어 (안전한 반복자) 🛡️
SCAN cursor [MATCH pattern] [COUNT count] 명령어를 사용하면 키를 점진적으로 순회(Iterate)할 수 있습니다.
작동 방식
한 번에 모든 것을 가져오는 대신, SCAN은 페이지네이션 시스템처럼 작동합니다.
- 요청:
SCAN 0을 보냅니다. - 응답: Redis는 아주 적은 양의 작업(내부 해시 테이블 버킷의 작은 섹션을 스캔)만 수행하고 다음을 반환합니다:
- 새로운 커서 (New Cursor) (예: "145").
- 해당 섹션에서 발견된 키 목록.
- 끼어들기 (Interleaving): 응답을 보낸 후, Redis는 자유로워져서 다른 요청(앱에서 오는
SET이나GET등)을 처리할 수 있습니다. - 다음 요청: 방금 받은 커서를 사용하여
SCAN 145를 보냅니다. - 반복: Redis가
0이라는 커서를 반환할 때까지 이를 반복합니다. (0은 순회가 끝났음을 의미).
안전한 이유
- 시간 복잡도: 호출당 $O(1)$. (엄밀히 말하면 $O(\text{COUNT})$). 전체 순회는 여전히 $O(N)$이지만, 아주 작은 조각들로 나뉘어 실행됩니다.
- 논블로킹 (Non-Blocking): 호출 사이에 스레드 점유를 해제하므로, 프로덕션 트래픽이
SCAN명령 사이사이에 "끼어들" 수 있습니다. 서버는 계속 응답 가능한 상태를 유지합니다.
SCAN의 특이점 (중요!)
SCAN은 상태가 없습니다(Redis는 당신이 어디까지 읽었는지 기억하지 않으며, _커서_가 위치 정보를 인코딩하고 있습니다). 이로 인해 몇 가지 부작용이 있습니다.
- 중복 (Duplicates): 서로 다른 두 번의
SCAN호출에서 동일한 키가 반환될 수 있습니다. 애플리케이션은 반드시 중복 제거를 처리해야 합니다(예: 키를 Set에 넣는 방식).- 이유: 스캔 중에 Redis가 해시 테이블 크기를 조정(Re-hashing)하면 버킷 구조가 바뀌기 때문입니다.
- 대략적인 개수:
COUNT옵션은 힌트일 뿐입니다. 10개를 요청했는데 15개를 받을 수도, 5개를 받을 수도, 혹은 0개를 받을 수도 있습니다(커서는 0이 아닌 상태로).
3. 나란히 비교
| 특징 | KEYS | SCAN |
|---|---|---|
| 블로킹 여부? | YES. 서버 전체를 멈춤. | NO. 호출 사이에 다른 명령 실행 가능. |
| 복잡도 | 한 번에 $O(N)$. | 호출당 $O(1)$ (전체 $O(N)$). |
| 반환 값 | 모든 키가 담긴 하나의 거대한 리스트. | 커서 + 작은 키 리스트. |
| 프로덕션 안전? | ❌ 아니오. | ✅ 예. |
| 중복 발생? | 없음. 유일성 보장. | 있음. 중복 가능성 존재. |
| 정확성 | 그 순간의 정확한 스냅샷. | 스캔 도중에 생성/삭제된 키는 반환될 수도, 안 될 수도 있음. |
4. 비유 🏟️
5만 명이 있는 거대한 경기장(Keys)에서 이름이 "철수"인 사람을 모두 찾는다고 상상해 봅시다.
- KEYS 방식: 마이크를 잡고 소리칩니다. "동작 그만! 아무도 움직이지 마! 이름이 철수인 사람 전부 무대 위로 올라와!"
- 경기가 중단됩니다. 아무도 핫도그를 살 수 없습니다. 선수들도 기다립니다. 5,000명의 철수가 내려올 때까지 당신은 기다립니다. 혼돈 그 자체입니다.
- SCAN 방식: 클립보드를 들고 한 줄씩(Row) 걸어 다닙니다. 첫 번째 줄에 가서 묻습니다. "여기 철수 있나요?" 그리고 명단에 적습니다.
- 당신이 다음 줄로 걸어가는 동안, 경기는 계속되고 사람들은 핫도그를 사며 경기장은 정상적으로 돌아갑니다. 결국 모든 철수를 찾아내겠지만, 많은 작은 발걸음이 필요합니다.
5. 구현 코드 (Python 예제)
특정 키를 안전하게 삭제하기 위해 스크립트에서 스캔을 구현하는 방법입니다.
import redis
r = redis.Redis(host='localhost', port=6379)
cursor = '0'
pattern = 'user:*:session'
while cursor != 0:
# SCAN은 [새_커서, [키_리스트]]를 반환합니다.
cursor, keys = r.scan(cursor=cursor, match=pattern, count=100)
if keys:
# 이 배치의 키들을 처리합니다.
r.delete(*keys)
print(f"{len(keys)}개의 키 배치를 삭제했습니다.")
print("정리 완료.")
references